package com.pinfly.common.http;

import java.io.IOException;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.security.cert.X509Certificate;

import org.apache.log4j.Logger;

import com.pinfly.common.config.ConfigException;
import com.pinfly.common.config.PfyProperties;
import com.pinfly.common.config.PfyPropertiesFactory;

/** Class to send an HTTPS request to a site and retrieve the response. */
public class HttpsClient extends HttpClient
{
    private static final Logger logger = Logger.getLogger (HttpsClient.class);

    // Our default protocol and port are different
    private final static String m_defaultProtocol = "https";
    private final static int m_defaultPort = 443;

    private static PfyProperties s_props = null;

    private SocketFactory m_socketFactory;
    private String[] m_validServerIds;

    static
    {
        try
        {
            s_props = PfyPropertiesFactory.getProperties ("ssl");
        }
        catch (ConfigException e)
        {
            logger.warn (e.getMessage ());
        }
    }

    /**
     * Create an HttpsClient to the specified host for the specified resource.
     * 
     * @param host a string specifying the host name
     * @param resource a string specifying the resource name
     */
    private HttpsClient (String host, String resource)
    {
        // set defaults
        m_protocol = m_defaultProtocol;
        m_port = m_defaultPort;

        // get specific info
        setHostAndPort (host);
        m_resource = resource;
    }

    /**
     * Create an HttpsClient to the specified host for the specified resource.
     * An initial query string can be included.
     * 
     * @param host a string specifying the host name
     * @param resource a string specifying the resource name
     * @param query query string part of request (e.g. "?p1=v1&p2=v2")
     */
    private HttpsClient (String host, String resource, String query)
    {
        // set defaults
        m_protocol = m_defaultProtocol;
        m_port = m_defaultPort;

        // get specific info
        setHostAndPort (host);
        m_resource = resource;
        m_query = query;
    }

    /**
     * Create an HttpsClient to the specified url.
     * 
     * @param url A string containing the entire url including protocol. The
     *            protocol must be https
     */
    private HttpsClient (String url)
    {
        // test for protocol
        if (!url.startsWith ("https://"))
        {
            throw new IllegalArgumentException ("Invalid protocol");
        }

        // finish the setup
        m_protocol = m_defaultProtocol;
        m_port = m_defaultPort;
        finishSetup (url.substring (8));
    }

    /**
     * Create an HttpsClient to the specified url.
     * <p>
     * This constructor is currently unusable because the URL class doesn't
     * support https as a protocol. The constructor is included here for
     * completeness so that if a future version of URL does support https this
     * constructor can be used.
     * 
     * @param url A URL containing the entire url including protocol. The
     *            protocol must be https.
     */
    private HttpsClient (URL url)
    {
        m_protocol = url.getProtocol ();
        if (!"https".equals (m_protocol))
        {
            throw new IllegalArgumentException ("Invalid protocol");
        }
        m_port = m_defaultPort;
        finishSetup (url);
    }

    public static HttpClient getInstance (String url)
    {
        return new HttpsClient (url);
    }

    /**
     * Set the list of valid server ids. The list of ids provided completely
     * replaces any previous list. Each entry in the list specifies a value
     * which may be present in the subject field of the server's certificate.
     * Each entry should be of the form name=value, where name is one of the
     * fields in an X500 distinguished name, and value is the value that the
     * field should have. For example <code>ou=CCOW.ContextManager</code>.
     * <p>
     * A server passes validation if any of the specified ids are in its subject
     * field.
     * 
     * @param ids The set of valid client ids
     * @throws IllegalArgumentException if any of the ids are not in the right
     *             format.
     * @see #setServerId
     */
    public void setServerIds (String[] ids)
    {
        if (ids != null && ids.length == 0)
            ids = null;
        m_validServerIds = ids;
        if (m_validServerIds != null)
        {
            for (int i = 0; i < m_validServerIds.length; i++)
            {
                m_validServerIds[i] = SSLUtil.validateId (m_validServerIds[i]);
            }
        }
    }

    /**
     * Set a valid server id. This is a convenience method equivalent to <code>
     * 	setServerIds (new String[] {id})</code>.
     * 
     * @param id The valid client id
     * @throws IllegalArgumentException if the id is not in the right format.
     * @see #setServerIds
     */
    public void setServerId (String id)
    {
        setServerIds (new String[]
        { id });
    }

    /**
     * Set client properties from the ssl.properties file. This method is
     * equivalent to setClientProperties (props, name), where props refers to
     * the ssl.properties file.
     * 
     * @param name The name to use in lookup up the properties
     * @throws IllegalArgumentException if the ssl.properties file doesn't
     *             contain all required properties.
     * @see #setClientProperties(MSProperties,String)
     */
    public void setClientProperties (String name)
    {
        setClientProperties (s_props, name);
    }

    /**
     * Set server properties from a properties file.
     * <p>
     * When retrieving values from the properties file, a search is first made
     * for &lt;name&gt;.&lt;property&gt;. If that is not found, then a search is
     * made for *.&lt;property&gt;. If that is not found and a default value is
     * defined it is used. If no value is specified and no default is defined,
     * an InvalidArgumentException is thrown.
     * <p>
     * The properties are
     * <table cellSpacing="0" cellPadding="3" border="1">
     * <tr class="TableHeadingColor" bgColor="#ccccff">
     * <td><b>Property </b></td>
     * <td><b>Description </b></td>
     * <td><b>Default </b></td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>KeyStoreFile</td>
     * <td>The name of the keystore file containing the certificate and
     * corresponding private key.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>PassPhrase</td>
     * <td>The pass phrase for retrieving the private key from the keystore.</td>
     * <td>required</td>
     * </tr>
     * <tr class="TableRowColor" bgColor="white">
     * <td>CertAlias</td>
     * <td>The alias that refers to the certificate and key in the keystore
     * file.</td>
     * <td>required</td>
     * </tr>
     * <tr>
     * <td>PeerId</td>
     * <td>The valid server id.</td>
     * <td>any server</td>
     * </tr>
     * </table>
     * 
     * @param props The properties to use.
     * @param name The name to use in looking up the properties.
     * @throws IllegalArgumentException if the properties file doesn't contain
     *             all required properties
     * @see #setClientProperties(String)
     */
    public void setClientProperties (PfyProperties props, String name)
    {
        // First get the require properties that define the certificate
        // and set up the socket factory to use that certificate.
        String keyStoreFile = SSLUtil.getProp (props, SSLUtil.KEY_STORE_FILE, name, null);
        String passPhrase = SSLUtil.getProp (props, SSLUtil.PASS_PHRASE, name, null);
        String certAlias = SSLUtil.getProp (props, SSLUtil.CERT_ALIAS, name, null);
        if (keyStoreFile == null || passPhrase == null || certAlias == null)
        {
            throw new IllegalArgumentException ("Properties file does not contain required properties: "
                                                + SSLUtil.KEY_STORE_FILE + ", " + SSLUtil.PASS_PHRASE + ", "
                                                + SSLUtil.CERT_ALIAS + '.');
        }
        try
        {
            SSLContext context = SSLUtil.getSSLContext (keyStoreFile, passPhrase, certAlias);
            m_socketFactory = context.getSocketFactory ();
        }
        catch (Exception e)
        {
            // Now what - no socket factory
            logger.fatal ("Couldn't create SecureServerSocketFactory for " + name, e);
        }

        String serverId = SSLUtil.getProp (props, SSLUtil.PEER_ID, name, null);
        if (serverId != null)
        {
            setServerId (serverId);
        }
    }

    /**
     * Create an SSL socket. This SSL handshake is completed before this method
     * returns. If server validation has been requested, then the server
     * certificate is validated.
     * 
     * @return The connected socket.
     * @throws MSHttpInvalidCertificateException if the server certificate
     *             failed validation.
     */
    protected Socket getSocket () throws UnknownHostException, IOException, MSHttpException
    {
        if (m_socketFactory == null)
        {
            try
            {
                // No client certificate was specified, so create a default
                // socket factory that doesn't include a client certificate
                m_socketFactory = SSLUtil.getDefaultSSLSocketFactory ();
            }
            catch (Exception e)
            {
                logger.warn ("Unable to get default SSLSocketFactory", e);
                throw new MSHttpException (e, "Failed to get default SSLSocketFactory");
            }
        }
        // Opens a socket for the url
        SSLSocket s = (SSLSocket) m_socketFactory.createSocket (getHost (), getPort ());

        logger.trace ("HttpsClient starting SSL Handshake");
        s.startHandshake ();
        logger.trace ("Client - handshake completed");
        if (m_validServerIds != null)
        {
            SSLSession session = s.getSession ();
            try
            {
                X509Certificate chain[] = session.getPeerCertificateChain ();
                X509Certificate srvrCert = chain[0];
                SSLUtil.validateCert (srvrCert, m_validServerIds);
            }
            catch (SSLPeerUnverifiedException e)
            {
                throw new MSHttpInvalidCertificateException ("No server certificate");
            }
        }
        return (s);
    }

    protected void releaseSocket (Socket sock)
    {
        // These sockets can't be re-used
        try
        {
            sock.close ();
        }
        catch (IOException ioe)
        {
            logger.warn ("Exception closing SSL socket to host " + m_host + " on port " + m_port, ioe);
        }
    }

    // public static void main (String[] args)
    // {
    // HttpClient clnt;
    // String resp;
    // try
    // {
    // HttpServer srvr = new HttpsServer ("ContextManager",
    // HttpServer.getDefaultProcessor(), 2443);
    // srvr.start ();
    // Logger.debug (1, "Server started");
    // clnt = HttpClient.getHttpClient ("https://localhost:2443");
    // if (clnt instanceof HttpsClient)
    // {
    // ((HttpsClient)clnt).setClientProperties ("testClient");
    // }
    // resp = clnt.get ("?arg1=val1&arg2=val2");
    // clnt.close();
    // Thread.sleep (2000);
    // clnt = HttpClient.getExclusiveHttpClient ("https://localhost:2443");
    // if (clnt instanceof HttpsClient)
    // {
    // ((HttpsClient)clnt).setClientProperties ("testClient");
    // }
    // resp = clnt.get ("?arg1=val1&arg2=val2");
    // clnt.close();
    // srvr.stop ();
    // Logger.debug (1, "Server stopped");
    // try
    // {
    // Class cl = Class.forName
    // ("com.sitraka.jprobe.profiler.api.JPPerformanceAPI");
    // Method m = cl.getDeclaredMethod ("getInstance", new Class[0]);
    // com.sitraka.jprobe.profiler.api.JPPerformanceAPI jpa =
    // (com.sitraka.jprobe.profiler.api.JPPerformanceAPI)m.invoke (null, new
    // Object[0]);
    // if (jpa.save ("SSL_Snapshot"))
    // {
    // System.out.println ("Snapshot created");
    // }
    // else
    // {
    // System.out.println ("Snapshot creation failed");
    // }
    // }
    // catch (Exception e)
    // {
    // System.out.println ("Didn't create snapshot because " + e.getMessage());
    // e.printStackTrace();
    // }
    //
    //
    // srvr = new HttpsServer ("ContextManager",
    // HttpServer.getDefaultProcessor(), 2443);
    // srvr.start ();
    // Logger.debug (1, "Server started");
    // clnt = HttpClient.getHttpClient ("https://localhost:2443");
    // if (clnt instanceof HttpsClient)
    // {
    // ((HttpsClient)clnt).setClientProperties ("testClient");
    // }
    // resp = clnt.get ("?arg1=val1&arg2=val2");
    // clnt.close();
    // srvr.stop ();
    // Logger.debug (1,"Server stopped");
    //
    // //Thread.sleep (3000);
    // try
    // {
    // Logger.debug (1, "Second call");
    // resp = clnt.get ("?arg3=val3&arg4=val4");
    // Logger.debug (1, "Response 2 was " + resp);
    // }
    // catch (Exception e)
    // {
    // Logger.debug (1, e, "Second call failed - this is good!");
    // }
    // srvr.start ();
    // Logger.debug (1, "Server restarted");
    // resp = clnt.get ("?arg5=val5&arg6=val6");
    // resp = clnt.get ("?arg7=val7&arg7=val7");
    // srvr.stop ();
    // Logger.debug (1, "Server stopped");
    // }
    // catch (Exception e)
    // {
    // e.printStackTrace();
    // }
    //
    // }

}
